/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.ui.dnd;
import java.awt.Cursor;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragSource;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.InputEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.util.FileSet;
import com.mucommander.commons.runtime.OsFamily;
import com.mucommander.job.impl.CopyJob;
import com.mucommander.job.impl.MoveJob;
import com.mucommander.job.impl.CopyJob.TransferMode;
import com.mucommander.text.Translator;
import com.mucommander.ui.dialog.file.FileCollisionDialog;
import com.mucommander.ui.dialog.file.ProgressDialog;
import com.mucommander.ui.main.FolderPanel;
import com.mucommander.ui.main.MainFrame;
/**
* Provides file(s) 'drop' support to components that add a <code>DropTarget</code> using this <code>DropTargetListener</code>.
* A {@link com.mucommander.ui.main.FolderPanel} instance has to be specified at creation time, this instance will be
* used to change the current folder, or copy/move files to the current folder.
*
* <p>There are 2 different modes this class can operate in. The mode to be used has to be specified when this class is
* instantiated.
*
* <p>In 'folder change mode', when a file or string representing a file path is dropped, the associated FolderPanel's
* current folder is changed:
* <ul>
* <li>If the file is a directory, the current folder is changed to that directory
* <li>For any other file kind (archive, regular file...), current folder is changed to the file's parent folder
* and the file is selected
* </ul>
* If more than one file (or file path) is dropped, only the first one is taken into account.
*
* <p>In the normal mode, files (or file paths) that are dropped can also be moved or copied to the associated FolderPanel's
* current folder, on top of the change current folder action. The actual drop action performed (move, copy or change current folder)
* depends on the keyboard modifiers typed by the user when dragging the files.
* When the mouse cursor enters the drop-enabled component's area, it is changed to symbolize the action to be performed.
* The default drop action (when no modifier is down) is copy.
*
* <p>Drop events originating from the same FolderPanel are on purpose not accepted as spring-loaded folders are not
* (yet) supported which would make the drop operation ambiguous and confusing.
*
* @author Maxence Bernard
*/
public class FileDropTargetListener implements DropTargetListener {
private static final Logger LOGGER = LoggerFactory.getLogger(FileDropTargetListener.class);
/** the FolderPanel instance used to change the current folder when a file is dropped */
private FolderPanel folderPanel;
/** Mode that specifies what to do when files are dropped */
private boolean changeFolderOnlyMode;
/** Drop action (copy or move) currenlty specified by the user */
private int currentDropAction;
/** Has DropTargetDragEvent event been accepted ? */
private boolean dragAccepted;
/**
* Extended modifiers which must be down while dragging for the drop action to be a MOVE and not a COPY (default):
* <code>InputEvent.META_DOWN_MASK</code> under Mac OS X, <code>InputEvent.ALT_DOWN_MASK</code> under any other
* platform.
*/
private final static int MOVE_ACTION_MODIFIERS_EX = OsFamily.MAC_OS_X.isCurrent()?
InputEvent.META_DOWN_MASK
:InputEvent.ALT_DOWN_MASK;
/**
* Creates a new FileDropTargetListener using the provided FolderPanel that will be used to either change the
* current folder or copy/move when files are dropped, depending on the specified operating mode and drop action.
*
* @param folderPanel the FolderPanel instance used to change the current folder or copy/move when files are dropped
* @param changeFolderOnlyMode if <code>true</code>, the FolderPanel's current folder can only be changed when file(s)
* are dropped, files cannot be copied or moved.
*/
public FileDropTargetListener(FolderPanel folderPanel, boolean changeFolderOnlyMode) {
this.folderPanel = folderPanel;
this.changeFolderOnlyMode = changeFolderOnlyMode;
}
/**
* Returns a mouse <code>Cursor<code> that symbolizes the given drop action and 'accepted' status.
* The given action must one of the following:
* <ul>
* <li>DnDConstants.ACTION_COPY
* <li>DnDConstants.ACTION_MOVE
* <li>DnDConstants.ACTION_LINK
* </ul>
* If the action has any other value, the default Cursor is returned.
*/
private Cursor getDragActionCursor(int dropAction, boolean dragAccepted) {
switch(dropAction) {
case DnDConstants.ACTION_COPY:
return dragAccepted?DragSource.DefaultCopyDrop:DragSource.DefaultCopyNoDrop;
case DnDConstants.ACTION_MOVE:
return dragAccepted?DragSource.DefaultMoveDrop:DragSource.DefaultMoveNoDrop;
case DnDConstants.ACTION_LINK:
return dragAccepted?DragSource.DefaultLinkDrop:DragSource.DefaultLinkNoDrop;
default:
return Cursor.getDefaultCursor();
}
}
/**
* Accepts or rejects the specified <code>DropTargetDragEvent</code> and changes the mouse cursor to match the
* current drop action.
* The drag event will be accepted it supports at least one of the supported DataFlavors and one of the two
* following conditions are true:
* <ul>
* <li>the event originates from one of muCommander's {@link FolderPanel} for which the current folder is not the
* same as the FolderPanel associated with this <code>FileDropTargetListener</code>
* <li>the event does not originate from muCommander
* </ul>
*
* <p>This method overrides the default drop action for drag-and-drop operations within muCommander to make it
* <code>DnDConstants.ACTION_COPY</code> instead of <code>DnDConstants.ACTION_MOVE</code>.
* For a move action to be performed when the mouse is released, the modifiers defined by
* {@link #MOVE_ACTION_MODIFIERS_EX} must be down.</p>
*
* @return <code>true</code> if the event was accepted, false otherwise
*/
private boolean acceptOrRejectDragEvent(DropTargetDragEvent event) {
this.currentDropAction = event.getDropAction();
this.dragAccepted = event.isDataFlavorSupported(TransferableFileSet.getFileSetDataFlavor())
|| event.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
|| event.isDataFlavorSupported(DataFlavor.getTextPlainUnicodeFlavor());
if(dragAccepted && DnDContext.isDragInitiatedByMucommander()) {
FolderPanel dragInitiator = DnDContext.getDragInitiator();
if(dragInitiator==folderPanel || dragInitiator.getCurrentFolder().equalsCanonical(folderPanel.getCurrentFolder())) {
// Refuse drag if the drag was initiated by the same FolderPanel, or if its current folder is the same
// as this one
this.dragAccepted = false;
}
else {
// Change the default drop action to DnDConstants.ACTION_COPY instead of DnDConstants.ACTION_MOVE,
// if the move extended modifiers are not currently down.
int dragModifiers = DnDContext.getDragGestureModifiersEx();
if(currentDropAction==DnDConstants.ACTION_MOVE
&& (dragModifiers&MOVE_ACTION_MODIFIERS_EX)==0
&& (event.getSourceActions()&DnDConstants.ACTION_COPY)!=0) {
LOGGER.debug("changing default action, was: DnDConstants.ACTION_MOVE, now: DnDConstants.ACTION_COPY");
currentDropAction = DnDConstants.ACTION_COPY;
}
}
}
LOGGER.trace("dragAccepted="+dragAccepted+" dropAction="+currentDropAction);
if(dragAccepted) {
// Accept the drag event with our drop action
event.acceptDrag(currentDropAction);
}
else {
// Reject the drag event
event.rejectDrag();
}
LOGGER.trace("cursor="+getDragActionCursor(currentDropAction, dragAccepted));
// Change the mouse cursor on this FolderPanel and child components
folderPanel.setCursor(getDragActionCursor(currentDropAction, dragAccepted));
return dragAccepted;
}
////////////////////////////////////
// FileDropTargetListener methods //
////////////////////////////////////
public void dragEnter(DropTargetDragEvent event) {
acceptOrRejectDragEvent(event);
}
public void dragOver(DropTargetDragEvent event) {
// Although it doesn't look necessary, cursor needs to be set each time this method is called otherwise
// it returns to the default one (at least under Mac OS X w/ Java 1.5)
acceptOrRejectDragEvent(event);
}
public void dropActionChanged(DropTargetDragEvent event) {
acceptOrRejectDragEvent(event);
}
public void dragExit(DropTargetEvent event) {
// Restore default cursor
folderPanel.setCursor(Cursor.getDefaultCursor());
}
public void drop(DropTargetDropEvent event) {
// Restore default cursor, no matter what
folderPanel.setCursor(Cursor.getDefaultCursor());
// The drop() method is called even if a DropTargetDropEvent was rejected before,
// so this test is really necessary
if(!dragAccepted) {
event.rejectDrop();
return;
}
// Accept drop event
event.acceptDrop(currentDropAction);
// Retrieve the files contained by the transferable as a FileSet (takes care of handling the different DataFlavors)
FileSet droppedFiles = TransferableFileSet.getTransferFiles(event.getTransferable());
// Stop and report failure if no file could not be retrieved
if(droppedFiles==null || droppedFiles.size()==0) {
// Report drop failure
event.dropComplete(false);
return;
}
// If in 'change folder mode' or if the drop action is 'ACTION_LINK' in normal mode:
// change the FolderPanel's current folder to the dropped file/folder :
// - If the file is a directory, the current folder is changed to that directory
// - For any other file kind (archive, regular file...), current folder is changed to the file's parent folder and the file is selected
// If more than one file is dropped, only the first one is used
if(changeFolderOnlyMode || currentDropAction==DnDConstants.ACTION_LINK) {
AbstractFile file = droppedFiles.elementAt(0);
// If file is a directory, change current folder to that directory
if(file.isDirectory())
folderPanel.tryChangeCurrentFolder(file);
// For any other file kind (archive, regular file...), change directory to the file's parent folder
// and select the file
else
folderPanel.tryChangeCurrentFolder(file.getParent(), file, false);
// Request focus on the FolderPanel
folderPanel.requestFocus();
}
// Normal mode: copy or move dropped files to the FolderPanel's current folder
else {
MainFrame mainFrame = folderPanel.getMainFrame();
AbstractFile destFolder = folderPanel.getCurrentFolder();
if(currentDropAction==DnDConstants.ACTION_MOVE) {
// Start moving files
ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("move_dialog.moving"));
MoveJob moveJob = new MoveJob(progressDialog, mainFrame, droppedFiles, destFolder, null, FileCollisionDialog.ASK_ACTION, false);
progressDialog.start(moveJob);
}
else {
// Start copying files
ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("copy_dialog.copying"));
CopyJob job = new CopyJob(progressDialog, mainFrame, droppedFiles, destFolder, null, TransferMode.COPY, FileCollisionDialog.ASK_ACTION);
progressDialog.start(job);
}
}
// Report that the drop event has been successfully handled
event.dropComplete(true);
}
}